﻿
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using XNADota.ContentPipeline.MDX;
using System;
using XNADota.Primitive;

// ReplaceableId 1代表隊伍顏色(純色無光暗)
// ReplaceableId 2代表隊伍顏色(有光暗多用於英雄之光)

namespace XNADota.MDX
{
    public static class Textures
    {
        public static void Initialize(ContentManager ContentManager)
        {
            TextureReplacableRed = ContentManager.Load<Texture2D>(TEXTURE_REPLACABLE_RED);
        }
        private const string TEXTURE_REPLACABLE_RED = "Textures\\ReplacableRed_Tga";
        public static Texture2D TextureReplacableRed;
    }

    /// <summary>
    /// It contains elements related to rendering
    /// </summary>
    public class ModelRenderer
    {
        private Model _Model;
        private List<GeosetRenderer> _GeosetRenderers;
        private List<NodeRenderer> _NodeRenderers;  // bones, helpers
        private List<int> _TopologicalIndex;
        private SequenceAnimationPlayer _SequenceAnimationPlayer;


        public SequenceAnimationPlayer Player
        {
            get { return _SequenceAnimationPlayer; }
        }

        public void SetAnimationClipIndex(int index)
        {
            _SequenceAnimationPlayer.SetAnimationClip(index);
        }

        // constructor
        public Model Model
        {
            set { _Model = value; }
            get { return _Model; }
        }

        public ModelRenderer(GraphicsDevice graphicsDevice, Model Model)
        {
            // validate the correction of the model
            try
            {
                if (Model.Validate() == false)
                    throw new InvalidModelException("Model is invalid.");
            }
            catch (System.Exception e)
            {
                throw new InvalidModelException("Model is invalid", e);
            }

            // build geoset related data
            this._Model = Model;
            this._GeosetRenderers = new List<GeosetRenderer>();

            foreach (Geoset geoset in _Model.Geosets)
            {
                this._GeosetRenderers.Add(new GeosetRenderer(graphicsDevice, geoset, this));
            }

            // build bones
            InitializeBonesAndHelpers();

            // build sequences
            _SequenceAnimationPlayer = new SequenceAnimationPlayer(_Model.Sequences);
        }

        private bool[] _Visited;
        private void InitializeBonesAndHelpers()
        {
            _NodeRenderers = new List<NodeRenderer>();
            _NodeRenderers.Clear();

            for (int i = 0; i < _Model.Bones.Count; i++)
                _NodeRenderers.Add(new BoneRenderer(_Model.Bones[i]));

            for (int i = 0; i < _Model.Helpers.Count; i++)
                _NodeRenderers.Add(new HelperRenderer(_Model.Helpers[i]));

            for (int i = 0; i < _Model.Helpers.Count; i++)
                if (_NodeRenderers[i].NodeId != i)
                    throw new InvalidModelException("Model format is incorrect.");

            // clear topological index array
            _TopologicalIndex = new List<int>();

            // clear visited array
            _Visited = new bool[_Model.Bones.Count + _Model.Helpers.Count];
            for (int i = 0; i < _Model.Bones.Count + Model.Helpers.Count; i++)
                _Visited[i] = false;

            // start topological
            for (int i = 0; i < _NodeRenderers.Count; i++)
            {
                TopologicalSort(_NodeRenderers[i]);
            }

            if (_TopologicalIndex.Count != _NodeRenderers.Count)
                throw new InvalidModelException("Count of topological index is incorrect.");

            _Visited = null;
        }
        private void TopologicalSort(NodeRenderer nodeRenderer)
        {
            // this is not all correct, because attachment and helper, what are added later, is not calculated.
            if (nodeRenderer.NodeId >= _Visited.Length || nodeRenderer.NodeId < 0)
            {
                throw new InvalidModelException("NodeRenderer index is out of range");
                //return;
            }

            if (_Visited[nodeRenderer.NodeId])
                return;
            else
            {
                if (nodeRenderer.ParentNodeId != -1)
                    TopologicalSort(_NodeRenderers[nodeRenderer.ParentNodeId]);

                _TopologicalIndex.Add(nodeRenderer.NodeId);
                _Visited[nodeRenderer.NodeId] = true;
            }
        }

        public void Update(GameTime time)
        {
            _SequenceAnimationPlayer.Update(time.ElapsedGameTime);
            int currentTick = _SequenceAnimationPlayer.CurrentTick;
            if (currentTick >= 0)
                UpdateAnimationTick(currentTick);
        }

        public void UpdateAnimationTick(int currentTick)
        {
            // update bone renderers
            for (int nodeIndex = 0; nodeIndex < _NodeRenderers.Count; nodeIndex++)
            {
                _NodeRenderers[nodeIndex].Update(currentTick);
            }

            // update world matrix
            for (int i = 0; i < _NodeRenderers.Count; i++)
            {
                // get current and parent index
                int current = _TopologicalIndex[i];
                int parent = _NodeRenderers[current].ParentNodeId;

                if (parent != -1)
                {
                    _NodeRenderers[current].World = _NodeRenderers[current].World * _NodeRenderers[parent].World;
                }
            }
        }

        /// <summary>
        /// Draws the model using the specified camera matrices.
        /// </summary>
        public void Draw(Matrix world, Matrix view, Matrix projection)
        {
            foreach (GeosetRenderer GeosetRenderer in _GeosetRenderers)
            {
                GeosetRenderer.NodeRenderer = _NodeRenderers;
                GeosetRenderer.Draw(world, view, projection);
            }
        }
    }

    public class GeosetRenderer
    {
        private Geoset _Geoset;

        private MaterialRenderer _MaterialRenderer;

        private VertexDeclaration VertexDeclaration;
        private VertexBuffer VertexBuffer;
        private IndexBuffer IndexBuffer;
        private BasicEffect BasicEffect;

        private List<NodeRenderer> _NodeRenderers;

        public List<NodeRenderer> NodeRenderer
        {
            set { _NodeRenderers = value; }
        }

        /// <summary>
        /// Once all the geometry has been specified by calling AddVertex and AddIndex,
        /// this method copies the vertex and index data into GPU format buffers, ready
        /// for efficient rendering.
        public GeosetRenderer(GraphicsDevice graphicsDevice, Geoset Geoset, ModelRenderer ModelRenderer)
        {
            this._Geoset = Geoset;

            this._MaterialRenderer = new MaterialRenderer(ModelRenderer, Geoset.MaterialID);

            // Create a vertex declaration, describing the format of our vertex data.
            VertexDeclaration = new VertexDeclaration(graphicsDevice,
                                                VertexPositionNormalTexture.VertexElements);

            // Create a vertex buffer, and copy our vertex data into it.
            VertexBuffer = new VertexBuffer(graphicsDevice,
                                            typeof(VertexPositionNormalTexture),
                                            _Geoset.Vertices.Length, BufferUsage.None);

            VertexBuffer.SetData(_Geoset.Vertices);

            // Create an index buffer, and copy our index data into it.
            IndexBuffer = new IndexBuffer(graphicsDevice, typeof(int),
                                          _Geoset.Indices.Length, BufferUsage.None);

            IndexBuffer.SetData(_Geoset.Indices);

            // Create a BasicEffect, which will be used to render the primitive.
            BasicEffect = new BasicEffect(graphicsDevice, null);

            BasicEffect.EnableDefaultLighting();
            BasicEffect.PreferPerPixelLighting = true;
        }

        public void SetRenderer(RenderState RenderState)
        {
            RenderState.FillMode = FillMode.Solid;
            RenderState.DepthBufferEnable = true;
            RenderState.DepthBufferWriteEnable = true;
            RenderState.CullMode = CullMode.None;
        }
        /// <summary>
        /// Draws the model using the specified camera matrices.
        /// </summary>
        public void Draw(Matrix world, Matrix view, Matrix projection)
        {
            // Look up the effect, and set effect parameters on it. This sample
            // assumes the model will only be using BasicEffect, but a more robust
            // implementation would probably want to handle custom effects as well.
            BasicEffect effect = this.BasicEffect;

            // lights
            //effect.EnableDefaultLighting();
            effect.DiffuseColor = new Vector3(0.5f, 0.5f, 0.5f);
            effect.SpecularColor = new Vector3(0.5f, 0.5f, 0.5f);
            effect.AmbientLightColor = new Vector3(0.8f, 0.8f, 0.8f);

            // coordinates
            effect.World = world;
            effect.View = view;
            effect.Projection = projection;

            effect.TextureEnabled = true;
            SetRenderer(effect.GraphicsDevice.RenderState);

            VertexPositionNormalTexture[] VerticesCopy = (VertexPositionNormalTexture[])_Geoset.Vertices.Clone();

            // cpu calculation of vertex position
            for (int vertexIndex = 0; vertexIndex < _Geoset.Vertices.Length; vertexIndex++)
            {
                int vertexGroupID = _Geoset.VertexGroupID[vertexIndex];
                List<int> groups = _Geoset.GeosetGroups[vertexGroupID];

                Matrix matrix = new Matrix();
                for (int groupIndex = 0; groupIndex < groups.Count; groupIndex++)
                {
                    int nodeIndex = groups[groupIndex];
                    matrix += _NodeRenderers[nodeIndex].World / (float)groups.Count;
                }

                VerticesCopy[vertexIndex].Position = Vector3.Transform(VerticesCopy[vertexIndex].Position, matrix);
            }
            VertexBuffer.SetData(VerticesCopy);

            // Set the graphics device to use our vertex declaration, vertex buffer, and index buffer.
            effect.GraphicsDevice.VertexDeclaration = this.VertexDeclaration;
            effect.GraphicsDevice.Vertices[0].SetSource(this.VertexBuffer, 0,
                                                 VertexPositionNormalTexture.SizeInBytes);
            effect.GraphicsDevice.Indices = this.IndexBuffer;

            var layerCount = this._MaterialRenderer.LayerRenderers.Length;
            for (var layerIndex = 0; layerIndex < layerCount; layerIndex++)
            {
                effect.Texture = this._MaterialRenderer.LayerRenderers[layerIndex].TextureRenderer.Texture;
                this._MaterialRenderer.LayerRenderers[layerIndex].SetRendererByFilterMode(effect.GraphicsDevice.RenderState);

                // Begin the effect, and loop over all the effect passes.
                effect.Begin();

                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Begin();

                    // Draw the geometry.
                    effect.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                                 0, 0, _Geoset.VerticesCount,
                                                 0, _Geoset.TriangleCount);

                    pass.End();
                }

                effect.End();
            }
        }
    }

    public class MaterialRenderer
    {
        private Material _Material;
        private LayerRenderer[] _LayerRenderers;

        public LayerRenderer[] LayerRenderers
        {
            get { return _LayerRenderers; }
        }

        public MaterialRenderer(ModelRenderer ModelRenderer, int MaterialID)
        {
            if (MaterialID < 0 || MaterialID > ModelRenderer.Model.Materials.Count)
                throw new InvalidModelException("Material ID is out of range, while constructing material renderer.");

            this._Material = ModelRenderer.Model.Materials[MaterialID];

            this._Material = ModelRenderer.Model.Materials[MaterialID];
            this._LayerRenderers = new LayerRenderer[_Material.Layers.Length];
            for (int layerIndex = 0; layerIndex < _Material.Layers.Length; layerIndex++)
            {
                this._LayerRenderers[layerIndex] = new LayerRenderer(ModelRenderer, _Material.Layers[layerIndex]);
            }
        }
    }

    public class LayerRenderer
    {
        private Layer _Layer;
        private TextureRenderer _TextureRenderer;

        public TextureRenderer TextureRenderer
        {
            get { return _TextureRenderer; }
        }

        public LayerRenderer(ModelRenderer ModelRenderer, Layer Layer)
        {
            _Layer = Layer;

            this._TextureRenderer = new TextureRenderer(ModelRenderer, Layer.TextureId);
        }

        public void SetRendererByFilterMode(RenderState renderState)
        {
            renderState.ColorWriteChannels = ColorWriteChannels.All;

            switch (_Layer.FilterMode)
            {
                case EMdxMaterialLayerFilterMode.Transparent:
                    renderState.AlphaTestEnable = true;
                    renderState.AlphaFunction = CompareFunction.Greater;
                    renderState.ReferenceAlpha = 192;

                    renderState.AlphaBlendEnable = false;
                    break;
                case EMdxMaterialLayerFilterMode.Blend:
                    renderState.AlphaBlendEnable = true;
                    renderState.SourceBlend = Blend.SourceAlpha;
                    renderState.DestinationBlend = Blend.InverseSourceAlpha;
                    renderState.BlendFunction = BlendFunction.Add;

                    break;
                case EMdxMaterialLayerFilterMode.Additive:
                    renderState.AlphaTestEnable = false;
                    renderState.AlphaBlendEnable = true;
                    renderState.SourceBlend = Blend.One;
                    renderState.DestinationBlend = Blend.One;
                    renderState.BlendFunction = BlendFunction.Add;
                    break;
                case EMdxMaterialLayerFilterMode.None:
                    renderState.AlphaTestEnable = false;
                    renderState.AlphaBlendEnable = false;
                    break;
                case EMdxMaterialLayerFilterMode.AdditiveAlpha:
                    renderState.AlphaBlendEnable = false;
                    renderState.SeparateAlphaBlendEnabled = true;
                    renderState.AlphaSourceBlend = Blend.One;
                    renderState.AlphaDestinationBlend = Blend.One;
                    renderState.AlphaBlendOperation = BlendFunction.Add;
                    break;
                default:
                    renderState.AlphaTestEnable = false;
                    renderState.AlphaBlendEnable = true;
                    renderState.SourceBlend = Blend.SourceAlpha;
                    renderState.DestinationBlend = Blend.InverseSourceAlpha;
                    break;
            }
        }
    }

    public class TextureRenderer
    {
        private MdxTexture _MdxTexture;
        private Texture2D _Texture2D;

        public Texture2D Texture
        {
            get { return _Texture2D; }
        }

        public TextureRenderer(ModelRenderer ModelRenderer, int TextureId)
        {
            if (TextureId < 0 || TextureId > ModelRenderer.Model.Textures.Count)
                throw new InvalidModelException("Texture ID is out of range, while constructing layer renderer.");

            this._MdxTexture = ModelRenderer.Model.Textures[TextureId];

            if (this._MdxTexture.Texture2D == null)
            {
                this._Texture2D = Textures.TextureReplacableRed;
            }
            else
            {
                this._Texture2D = this._MdxTexture.Texture2D as Texture2D;
            }
        }
    }

    public class BoneRenderer : NodeRenderer
    {
        private int _GeosetId;

        public BoneRenderer(Bone bone)
            : base(bone)
        {
            _GeosetId = bone.GeosetId;
        }
    }

    public class HelperRenderer : NodeRenderer
    {
        public HelperRenderer(Helper helper)
            : base(helper)
        {
            // emtpy
        }
    }

    public class NodeRenderer
    {
        private Node _Node;

        private Matrix _Translation;
        private Matrix _Rotation;
        private Matrix _Scaling;

        private Matrix _World;

        public Vector3 PivorPoint
        {
            get { return _Node.PivorPoint; }
        }

        public Node Node
        {
            get { return _Node; }
        }
        public int NodeId
        {
            get { return _Node.NodeId; }
        }
        public int ParentNodeId
        {
            get { return _Node.ParentNodeId; }
        }

        public Matrix Translation
        {
            set { _Translation = value; }
            get { return _Translation; }
        }
        public Matrix Rotation
        {
            set { _Rotation = value; }
            get { return _Rotation; }
        }
        public Matrix Scaling
        {
            set { _Scaling = value; }
            get { return _Scaling; }
        }
        public Matrix World
        {
            set { _World = value; }
            get { return _World; }
        }

        public NodeRenderer(Node node)
        {
            this._Node = node;
        }


        public void Update(int CurrentKeyFrame)
        {
            this._Translation = GetAnimatorMatrix(this._Node.Translation, EAnimateType.Translation, CurrentKeyFrame);
            this._Rotation = GetAnimatorMatrix(this._Node.Rotation, EAnimateType.Rotation, CurrentKeyFrame);
            this._Scaling = GetAnimatorMatrix(this._Node.Scaling, EAnimateType.Scaling, CurrentKeyFrame);

            Vector3 pivor = Vector3.Transform(this._Node.PivorPoint, this._Translation);
            this._World = Matrix.CreateTranslation(-pivor) *
                          this.Rotation * this._Scaling *
                          Matrix.CreateTranslation(pivor);
        }

        private enum EAnimateType
        {
            Rotation,
            Scaling,
            Translation,
        }

        private Vector3 ToVector3(Vector4 Vector4)
        {
            return new Vector3(Vector4.X, Vector4.Y, Vector4.Z);
        }

        private Matrix GetAnimatorMatrix(Animator animator, EAnimateType type, int CurrentKeyFrame)
        {
            int length = animator.AnimatorNodes.Length;
            if (length == 0)
                return Matrix.Identity;

            if (CurrentKeyFrame < animator.AnimatorNodes[0].Time || CurrentKeyFrame >= animator.AnimatorNodes[length - 1].Time)
                return Matrix.Identity;

            int index = 0;
            for (index = 0; index < animator.AnimatorNodes.Length; index++)
                if (animator.AnimatorNodes[index].Time <= CurrentKeyFrame && CurrentKeyFrame < animator.AnimatorNodes[index + 1].Time) break;

            AnimatorNode current = animator.AnimatorNodes[index];
            AnimatorNode next = animator.AnimatorNodes[index + 1];
            float amount = (float)(CurrentKeyFrame - current.Time) / (float)(next.Time - current.Time);
            Vector4 result = new Vector4();

            animator.Type = EMdxInterpolationType.Linear;
            switch (animator.Type)
            {
                case EMdxInterpolationType.None:
                    result = current.Value;
                    break;
                case EMdxInterpolationType.Linear:
                    result = Vector4.Lerp(current.Value, next.Value, amount);
                    break;
                case EMdxInterpolationType.Hermite:
                    if (type == EAnimateType.Rotation)
                        result = Vector4.Hermite(current.Value, current.OutTangent, next.Value, next.InTangent, amount);
                    else
                        result = new Vector4(Vector3.Hermite(ToVector3(current.Value), ToVector3(current.OutTangent),
                                                             ToVector3(next.Value), ToVector3(next.InTangent), amount), 0);
                    break;
                case EMdxInterpolationType.Bezier:
                    if (type == EAnimateType.Rotation)
                        result = MathHelper.InterpolateBezier(current.Value, current.OutTangent, next.Value, next.InTangent, amount);
                    else
                        result = MathHelper.InterpolateBezier(current.Value, current.OutTangent, next.Value, next.InTangent, amount);
                    //result = Vector4.Lerp(current.Value, next.Value, amount);
                    //throw new Exception("Currently it does not support bezier interpolation.");
                    // use linear instead
                    break;
                default:
                    throw new Exception("Incorrect interpolation type is got.");
            }

            switch (type)
            {
                case EAnimateType.Rotation:
                    return Matrix.CreateFromQuaternion(new Quaternion(result.X, result.Y, result.Z, result.W));
                case EAnimateType.Translation:
                    return Matrix.CreateTranslation(result.X, result.Y, result.Z);
                case EAnimateType.Scaling:
                    return Matrix.CreateScale(result.X, result.Y, result.Z);
                default:
                    throw new Exception("Incorrect animator type is got.");
            }
        }
    }
    public class SequenceAnimationPlayer
    {
        private List<Sequence> _Sequences;

        private bool _IsPlaying = false;
        private int _CurrentTick;
        private float _AnimateSpeed = 1.0f;
        private int _CurrnetClipIndex = 0;

        public int CurrentTick
        {
            get { return _CurrentTick; }
        }

        public bool IsPlaying
        {
            get { return _IsPlaying; }
        }

        public SequenceAnimationPlayer(List<Sequence> sequences)
        {
            _Sequences = sequences;
            Reset();
        }

        private void Reset()
        {
            _IsPlaying = false;
            _CurrentTick = _Sequences[_CurrnetClipIndex].IntervalStart;
            _AnimateSpeed = 1.0f;
        }

        public void SetAnimationClip(int clipIndex)
        {
            _CurrnetClipIndex = clipIndex;
            _CurrentTick = _Sequences[_CurrnetClipIndex].IntervalStart;
        }

        public void Update(TimeSpan elapsedTime)
        {
            if (_IsPlaying)
            {
                StepForward((int)(elapsedTime.TotalSeconds * 1000 * _AnimateSpeed));
            }
        }

        public void StepForward(int steps)
        {
            _CurrentTick += steps;
            Sequence sequence = _Sequences[_CurrnetClipIndex];
            if (_CurrentTick < _Sequences[_CurrnetClipIndex].IntervalStart)
            {
                _CurrentTick = (_CurrentTick - sequence.IntervalStart) % (sequence.IntervalEnd - sequence.IntervalStart) +
                    sequence.IntervalEnd;
            }
            if (_CurrentTick >= sequence.IntervalEnd)
            {
                if (sequence.NonLooping)
                    Stop();
                else
                    _CurrentTick = (_CurrentTick - sequence.IntervalEnd) % (sequence.IntervalEnd - sequence.IntervalStart) +
                        sequence.IntervalStart;
            }
        }

        public void Start(float AnimationSpeed)
        {
            _CurrentTick = _Sequences[_CurrnetClipIndex].IntervalStart;
            _IsPlaying = true;
            this._AnimateSpeed = AnimationSpeed;
        }

        public void Stop()
        {
            Reset();
        }
    }

    public static class MathHelper
    {
        public static Vector4 InterpolateBezier(Vector4 value1, Vector4 outTangent, Vector4 value2, Vector4 inTangent, float Factor)
        {
            float FactorX2 = Factor * Factor;
            float InverseFactor = 1.0f - Factor;
            float InverseFactorX2 = InverseFactor * InverseFactor;

            float Factor1 = InverseFactorX2 * InverseFactor;
            float Factor2 = 3.0f * Factor * InverseFactorX2;
            float Factor3 = 3.0f * FactorX2 * InverseFactor;
            float Factor4 = FactorX2 * Factor;

            return (value1 * Factor1) + (outTangent * Factor2) + (inTangent * Factor3) + (value2 * Factor4);
        }

        public static Vector3 InterpolateBezier(Vector3 value1, Vector3 outTangent, Vector3 value2, Vector3 inTangent, float Factor)
        {
            float FactorX2 = Factor * Factor;
            float InverseFactor = 1.0f - Factor;
            float InverseFactorX2 = InverseFactor * InverseFactor;

            float Factor1 = InverseFactorX2 * InverseFactor;
            float Factor2 = 3.0f * Factor * InverseFactorX2;
            float Factor3 = 3.0f * FactorX2 * InverseFactor;
            float Factor4 = FactorX2 * Factor;

            return (value1 * Factor1) + (outTangent * Factor2) + (inTangent * Factor3) + (value2 * Factor4);
        }
    }
}
